Lambdaが標準で持ってるAWS-SDKのバージョンをTweetするBotを作ってみた
こんにちは、CX事業本部の夏目です。
Lambdaに最初から入ってるAWS-SDKのバージョンが気になったので、一日一回TweetしてくれるBotを作ったので、構成や工夫を紹介します。
構成
- AWS-SDKのバージョンを取得するLambdaはCloudWatchのScheduleで一日一回動く
- AWS-SDKのバージョンを取得するLambdaとTweetするLambda(Tweeter)は
Lambda Destination
でつないで、別々に実装する - AWS-SDKのバージョンを取得するLambdaはログを出力させない
- TwitterのAPI KeyなどはSystems Manager Parameter StoreにKMSで暗号化して保存
- TweetするLambda(Tweeter)については簡易的なエラー通知を行う
AWS-SDKのバージョンを取得するLambdaはログを出力させない
すごくシンプルな処理かつ通信を行うわけでもないので、ログ出力そのものをさせない。
(今回の使い方であればCloudWatch Logsの料金は気になるほどでないが、そういう使い方もあるんだよという例と思って欲しい)
TweetするLambda(Tweeter)については簡易的なエラー通知を行う
TweeterのLambdaについてはエラーが起きた際に通知を行うようにしている。
MetricsFilterを使用してCloudWatch Logsにエラーログが出力されると、CustomMetricsを投げるようにしている。
投げられたCustomMetricsをCloudWatch Alarmで監視をして、何かあればSNS TopicにPublishをする。
残念ながら、AWSの規約の関係上SNS Topicの次に使っているサービスについては紹介できない。
しかし、SNS TopicをサブスクライブしてSlackへの通知処理を実装すると考えてもらえばよい。
リポジトリ
. ├── .circleci │ └── config.yml # deployはCircleCIで ├── .gitignore ├── LICENSE # GitHubが生成してくれるやつ ├── Makefile # Task Runnerとして使う ├── README.md ├── poetry.lock # poetryのlockファイル ├── pyproject.toml # poetryの設定ファイル ├── sam.yml └── src ├── layer │ ├── Dockerfile │ └── requirements.txt ├── node │ └── index.js ├── python │ └── index.py ├── ruby │ └── index.rb └── tweet ├── index.py └── main.py 6 directories, 13 files
リポジトリの構成は上記のようになっている。
src/tweet
ディレクトリにTweeterの実装を、src/layer
ディレクトリにLambda Layerの実装を置く想定。
(Lambda Layerの実装は make build
実行時に置かれる)
すべてを説明していたら長くなってしまうので、一部項目について説明する。
Makefile
SHELL = /usr/bin/env bash -xeuo pipefail stack_name:=lambda-runtime-tweeter-lambda template_path:=packaged.yml isort: poetry run isort -rc src black: poetry run black src format: isort black build: cd src/layer; \ docker build -t my-build .; \ docker run --name my-container my-build pip3 install -r requirements.txt -t ./python; \ docker cp my-container:/workdir/python .; \ docker rm my-container; \ docker rmi my-build; \ cd ../../ package: poetry run sam package \ --s3-bucket $$ARTIFACT_BUCKET \ --output-template-file $(template_path) \ --template-file sam.yml deploy: package poetry run sam deploy \ --stack-name $(stack_name) \ --template-file $(template_path) \ --capabilities CAPABILITY_IAM \ --no-fail-on-empty-changeset \ --role-arn $$CLOUDFORMATION_DEPLOY_ROLE_ARN .PHONY: \ deploy \ package \ build \ isort \ black \ format
SAMのデプロイ等はmakeコマンドを使って実行する。
実際のデプロイはCircleCI上で行うため、Lambdaのデプロイパッケージを保存するS3 Bucket名やCloudFormationを実行するIAM RoleのARNは環境変数で渡す。
(CircleCIの環境変数を設定できるのはリポジトリでAdminの権限を持ってる人だけ、だったはず。自信ない)
(いつの間にか、実行時のログにも出力しないようになってた)
こうやって、隠すべき情報はGitに載せないことで、パブリックリポジトリに安心して置くことができる。
build
ではCircleCI上での動作を想定して、Docker Imageの作成と実行、実行したコンテナからのファイルコピーで依存ファイルを手元に持ってきている。
(CircleCIではdockerコマンドが実行されるホストが別なので、-v
オプションとかでファイルをマウントすることができない)
(Docker Imageを作成する際には別ホストでもローカルのファイルをコピーしてくれるのを利用してファイルを送り込んでいる)
.circleci/config.yml
version: 2 jobs: deploy: docker: - image: circleci/python:3.8.1 steps: - run: name: poetry in-project true command: | set -x poetry config virtualenvs.in-project true - checkout - setup_remote_docker - restore_cache: keys: - layer-{{ checksum "src/layer/Dockerfile" }}-{{ checksum "src/layer/requirements.txt" }} - restore_cache: keys: - poetry-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }} - run: name: install dependencies command: | set -x poetry install if [ ! -d src/layer/python ]; then make build fi - save_cache: paths: - .venv key: poetry-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }} - save_cache: paths: - src/layer/python key: layer-{{ checksum "src/layer/Dockerfile" }}-{{ checksum "src/layer/requirements.txt" }} - run: name: install dependencies command: | set -x make deploy workflows: version: 2 deploy: jobs: - deploy: filters: branches: only: master
pipモジュールの管理にはpoetryを使用しています。
poetry config virtualenvs.in-project true
と設定しておくとprojectのルートディレクトリに.venv
という仮想環境を作成するので、CircleCIでキャッシュさせておく。
キャッシュが残っていればpoetryはpipモジュール再インストールを行わない。
キャッシュのキーとして、pyproject.toml
とpoetry.lock
のチェックサムを使用しているので、変更があった際にはキャッシュがアップデートされる。
Lambda LayerにするpipモジュールもCircleCIでキャッシュさせておく。
src/tweet/main.py
import boto3 from botocore.client import BaseClient from jeffy.framework import setup from twitter import Api app = setup() def main(event: dict, ssm_client: BaseClient = boto3.client("ssm")): app.logger.info({"name": "event", "value": event}) keys = get_keys(ssm_client) message = get_message(event) tweet(message, keys) def get_keys(ssm_client: BaseClient) -> dict: option = {"Path": "/twitter/keys", "WithDecryption": True} resp = ssm_client.get_parameters_by_path(**option) keys = {x["Name"]: x["Value"] for x in resp["Parameters"]} return { "consumer_key": keys["/twitter/keys/consumer_key"], "consumer_secret": keys["/twitter/keys/consumer_secret"], "access_token_key": keys["/twitter/keys/access_token_key"], "access_token_secret": keys["/twitter/keys/access_token_secret"], } def get_message(event: dict) -> str: return event["responsePayload"] def tweet(message: str, keys: dict): resp = Api(**keys).PostUpdate(message) app.logger.info({"name": "tweet response", "value": resp})
TwitterのAPI Keyは4つ必要になるのだが、それぞれ Systems Manager Parameter StoreにKMSで暗号化して保存している。
- consumer_key:
/twitter/keys/consumer_key
- consumer_secret:
/twitter/keys/consumer_secret
- access_token_key:
/twitter/keys/access_token_key
- access_token_secret:
/twitter/keys/access_token_secret
それぞれのキーを上記名前で保存しているのだが、このときGetParametersByPath
というAPIを使用してまとめて値を取得している。
(正確にはまとめて値を取得できるようにこのような名前にした)
また、取得時に"WithDecryption": True
を指定することで、復号化された値を取得できる。
まとめ
ということで、以上作ったものの工夫等でした。
これらの工夫が何かのやくにたてば幸いです。